package com.bankcard.bininfo.model;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * Utility class for managing card brands.
 * This class provides methods for registering, retrieving, and managing card brands.
 */
public final class CardBrands {
    public static final CardBrand VISA = new CardBrandImpl("Visa", "visa");
    public static final CardBrand MASTERCARD = new CardBrandImpl("Mastercard", "mastercard");
    public static final CardBrand AMEX = new CardBrandImpl("American Express", "amex");
    public static final CardBrand DISCOVER = new CardBrandImpl("Discover", "discover");
    public static final CardBrand JCB = new CardBrandImpl("JCB", "jcb");
    public static final CardBrand UNIONPAY = new CardBrandImpl("UnionPay", "unionpay");
    public static final CardBrand DINERS_CLUB = new CardBrandImpl("Diners Club", "diners");
    public static final CardBrand MAESTRO = new CardBrandImpl("Maestro", "maestro");
    public static final CardBrand RUPAY = new CardBrandImpl("RuPay", "rupay");
    public static final CardBrand MIR = new CardBrandImpl("Mir", "mir");
    public static final CardBrand EFTPOS = new CardBrandImpl("EFTPOS", "eftpos");
    public static final CardBrand INTERAC = new CardBrandImpl("Interac", "interac");
    public static final CardBrand TROY = new CardBrandImpl("Troy", "troy");
    public static final CardBrand VERVE = new CardBrandImpl("Verve", "verve");
    public static final CardBrand GIROCARD = new CardBrandImpl("Girocard", "girocard");
    public static final CardBrand BANCONTACT = new CardBrandImpl("Bancontact", "bancontact");
    public static final CardBrand CARTE_BANCAIRE = new CardBrandImpl("Carte Bancaire", "cb");
    public static final CardBrand MADA = new CardBrandImpl("mada", "mada");
    public static final CardBrand ALIPAY = new CardBrandImpl("Alipay", "alipay");
    public static final CardBrand WECHAT_PAY = new CardBrandImpl("WeChat Pay", "wechatpay");
    public static final CardBrand CHINA_T_UNION = new CardBrandImpl("China T-Union", "ctunion");
    public static final CardBrand CHINA_UNIONPAY_DEBIT = new CardBrandImpl("UnionPay Debit", "unionpay_debit");
    public static final CardBrand CHINA_UNIONPAY_CREDIT = new CardBrandImpl("UnionPay Credit", "unionpay_credit");

    private static final ConcurrentMap<String, CardBrand> CARD_BRANDS = new ConcurrentHashMap<>();

    static {
        register(VISA);
        register(MASTERCARD);
        register(AMEX);
        register(DISCOVER);
        register(JCB);
        register(UNIONPAY);
        register(DINERS_CLUB);
        register(MAESTRO);
        register(RUPAY);
        register(MIR);
        register(EFTPOS);
        register(INTERAC);
        register(TROY);
        register(VERVE);
        register(GIROCARD);
        register(BANCONTACT);
        register(CARTE_BANCAIRE);
        register(MADA);
        register(ALIPAY);
        register(WECHAT_PAY);
        register(CHINA_T_UNION);
        register(CHINA_UNIONPAY_DEBIT);
        register(CHINA_UNIONPAY_CREDIT);
    }

    private CardBrands() {}

    /**
     * Registers a new card brand.
     *
     * @param cardBrand The CardBrand to be registered.
     */
    public static void register(CardBrand cardBrand) {
        CARD_BRANDS.putIfAbsent(cardBrand.getCode().toLowerCase(), cardBrand);
    }

    /**
     * Registers a new card brand from a JSON string.
     *
     * @param json A JSON string containing the card brand information.
     * @return The newly registered CardBrand.
     * @throws IllegalArgumentException If the JSON format is invalid.
     *
     * @example
     * String json = "{\"name\":\"New Brand\",\"code\":\"newbrand\"}";
     * CardBrand newBrand = CardBrands.registerFromJson(json);
     */
    public static CardBrand registerFromJson(String json) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            JsonNode node = mapper.readTree(json);

            String name = node.get("name").asText();
            String code = node.get("code").asText();

            CardBrand newBrand = new CardBrandImpl(name, code);

            return CARD_BRANDS.computeIfAbsent(code.toLowerCase(), k -> newBrand);
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException("Invalid JSON format", e);
        }
    }

    /**
     * Retrieves a CardBrand by its code.
     *
     * @param code The code of the CardBrand to retrieve.
     * @return The CardBrand associated with the given code, or null if not found.
     *
     * @example
     * CardBrand visa = CardBrands.getByCode("visa");
     */
    public static CardBrand getByCode(String code) {
        return CARD_BRANDS.get(code.toLowerCase());
    }

    /**
     * Returns the CardBrand instance with the specified code.
     * This method is similar to the valueOf method in Enum classes.
     *
     * @param code The code of the CardBrand to retrieve.
     * @return The CardBrand instance associated with the given code.
     * @throws IllegalArgumentException if the code is not recognized.
     */
    public static CardBrand valueOf(String code) {
        CardBrand brand = CardBrands.getByCode(code);
        if (brand == null) {
            throw new IllegalArgumentException("No CardBrand with code " + code + " found");
        }
        return brand;
    }

    /**
     * Returns an array of all CardBrand instances.
     *
     * @return An array of CardBrand instances.
     */
    public static CardBrand[] values() {
        return CARD_BRANDS.values().toArray(new CardBrand[0]);
    }

    private static class CardBrandImpl implements CardBrand {
        private final String name;
        private final String code;

        CardBrandImpl(String name, String code) {
            this.name = name;
            this.code = code;
        }

        @Override
        public String getName() {
            return name;
        }

        @Override
        public String getCode() {
            return code;
        }
    }
}